/*
*********************************************************************
 * File:    ximawmf.cpp
 * Purpose:    Windows Metafile Class Loader and Writer
 * Author:    Volker Horch - vhorch@gmx.de
 * created:    13-Jun-2002
 *
 * Note:    If the code below works, i wrote it.
 *            If it doesn't work, i don't know who wrote it.
*********************************************************************
 */

/*
*********************************************************************
    Note by Author:
*********************************************************************

    Metafile Formats:
    =================

    There are 2 kinds of Windows Metafiles:
    - Standard Windows Metafile
    - Placeable Windows Metafile

    A StandardWindows Metafile looks like:
    - Metafile Header (MEATAHEADER)
    - Metafile Records 

    A Placeable Metafile looks like:
    - Aldus Header (METAFILEHEADER)
    - Metafile Header (METAHEADER)
    - Metafile Records

    The "Metafile Header" and the "Metafile Records" are the same
    for both formats. However, the Standard Metafile does not contain any
    information about the original dimensions or x/y ratio of the Metafile.

    I decided, to allow only placeable Metafiles here. If you also want to
    enable Standard Metafiles, you will have to guess the dimensions of
    the image.

*********************************************************************
    Limitations:    see ximawmf.h
                    you may configure some stuff there
*********************************************************************
*/

#include "ximawmf.h"

#if CXIMAGE_SUPPORT_WMF && CXIMAGE_SUPPORT_WINDOWS

////////////////////////////////////////////////////////////////////////////////
#if CXIMAGE_SUPPORT_DECODE
////////////////////////////////////////////////////////////////////////////////
bool CxImageWMF::Decode(CxFile *hFile, int32_t nForceWidth, int32_t nForceHeight)
{
    if (hFile == nullptr) return false;

    HENHMETAFILE    hMeta;
    HDC                hDC;
    int32_t                cx,cy;

    //save the current position of the file
    int32_t pos = hFile->Tell();

    // Read the Metafile and convert to an Enhanced Metafile
    METAFILEHEADER    mfh;
    hMeta = ConvertWmfFiletoEmf(hFile, &mfh);
    if (hMeta) {    // ok, it's a WMF

/////////////////////////////////////////////////////////////////////
//    We use the original WMF size information, because conversion to
//    EMF adjusts the Metafile to Full Screen or does not set rclBounds at all
//    ENHMETAHEADER    emh;
//    uint32_t            uRet;
//    uRet = GetEnhMetaFileHeader(hMeta,                    // handle of enhanced metafile 
//                                sizeof(ENHMETAHEADER),    // size of buffer, in bytes 
//                                &emh);                     // address of buffer to receive data  
//    if (!uRet){
//        DeleteEnhMetaFile(hMeta);
//        return false;
//    }
//    // calculate size
//    cx = emh.rclBounds.right - emh.rclBounds.left;
//    cy = emh.rclBounds.bottom - emh.rclBounds.top;
/////////////////////////////////////////////////////////////////////

        // calculate size
        // scale the metafile (pixels/inch of metafile => pixels/inch of display)
        // mfh.inch already checked to be <> 0

        hDC = ::GetDC(0);
        int32_t cx1 = ::GetDeviceCaps(hDC, LOGPIXELSX);
        int32_t cy1 = ::GetDeviceCaps(hDC, LOGPIXELSY);
        ::ReleaseDC(0, hDC);

        cx = (mfh.inch/2 + (mfh.bbox.right - mfh.bbox.left) * cx1) / mfh.inch;
        cy = (mfh.inch/2 + (mfh.bbox.bottom - mfh.bbox.top) * cy1) / mfh.inch;

    } else {        // maybe it's an EMF...

        hFile->Seek(pos,SEEK_SET);

        ENHMETAHEADER    emh;
        hMeta = ConvertEmfFiletoEmf(hFile, &emh);

        if (!hMeta){
            strcpy_s(info.szLastError,"corrupted WMF");
            return false; // definitively give up
        }

        // ok, it's an EMF; calculate canvas size
        cx = emh.rclBounds.right - emh.rclBounds.left;
        cy = emh.rclBounds.bottom - emh.rclBounds.top;

        // alternative methods, sometime not so reliable... [DP]
        //cx = emh.szlDevice.cx;
        //cy = emh.szlDevice.cy;
        //
        //hDC = ::GetDC(0);
        //float hscale = (float)GetDeviceCaps(hDC, HORZRES)/(100.0f * GetDeviceCaps(hDC, HORZSIZE));
        //float vscale  =  (float)GetDeviceCaps(hDC, VERTRES)/(100.0f * GetDeviceCaps(hDC, VERTSIZE));
        //::ReleaseDC(0, hDC);
        //cx = (int32_t)((emh.rclFrame.right - emh.rclFrame.left) * hscale);
        //cy = (int32_t)((emh.rclFrame.bottom - emh.rclFrame.top) * vscale);
    }

    if (info.nEscape == -1) {    // Check if cancelled
        head.biWidth = cx;
        head.biHeight= cy;
        info.dwType = CXIMAGE_FORMAT_WMF;
        DeleteEnhMetaFile(hMeta);
        strcpy_s(info.szLastError,"output dimensions returned");
        return true;
    }

    if (!cx || !cy)    {
        DeleteEnhMetaFile(hMeta);
        strcpy_s(info.szLastError,"empty WMF");
        return false;
    }

    if (nForceWidth) cx=nForceWidth;
    if (nForceHeight) cy=nForceHeight;
    ShrinkMetafile(cx, cy);        // !! Otherwise Bitmap may have bombastic size

    HDC hDC0 = ::GetDC(0);    // DC of screen
    HBITMAP hBitmap = CreateCompatibleBitmap(hDC0, cx, cy);    // has # colors of display
    hDC = CreateCompatibleDC(hDC0);    // memory dc compatible with screen
    ::ReleaseDC(0, hDC0);    // don't need anymore. get rid of it.

    if (hDC){
        if (hBitmap){
            RECT rc = {0,0,cx,cy};
            int32_t bpp = ::GetDeviceCaps(hDC, BITSPIXEL);

            HBITMAP hBitmapOld = (HBITMAP)SelectObject(hDC, hBitmap);

            // clear out the entire bitmap with windows background
            // because the MetaFile may not contain background information
            uint32_t    dwBack = XMF_COLOR_BACK;
#if XMF_SUPPORT_TRANSPARENCY
            if (bpp == 24) dwBack = XMF_COLOR_TRANSPARENT;
#endif
            uint32_t OldColor = SetBkColor(hDC, dwBack);
            ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rc, nullptr, 0, nullptr);
            SetBkColor(hDC, OldColor);

            //retrieves optional palette entries from the specified enhanced metafile
            PLOGPALETTE plogPal;
            PBYTE pjTmp; 
            HPALETTE hPal; 
            int32_t iEntries = GetEnhMetaFilePaletteEntries(hMeta, 0, nullptr);
            if (iEntries) { 
                if ((plogPal = (PLOGPALETTE)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, 
                    sizeof(uint32_t) + sizeof(PALETTEENTRY)*iEntries )) == nullptr) { 
                    DeleteObject(hBitmap);
                    DeleteDC(hDC);
                    DeleteEnhMetaFile(hMeta);
                    strcpy_s(info.szLastError,"Cancelled");
                    return false;
                } 

                plogPal->palVersion = 0x300; 
                plogPal->palNumEntries = (uint16_t) iEntries; 
                pjTmp = (PBYTE) plogPal; 
                pjTmp += 4; 

                GetEnhMetaFilePaletteEntries(hMeta, iEntries, (PPALETTEENTRY)pjTmp); 
                hPal = CreatePalette(plogPal); 
                GlobalFree(plogPal); 

                SelectPalette(hDC, hPal, FALSE); 
                RealizePalette(hDC); 
            } 
            
            // Play the Metafile into Memory DC
            BOOL bRet = PlayEnhMetaFile(hDC,    // handle to a device context 
                                    hMeta,    // handle to an enhanced metafile  
                                    &rc);     // pointer to bounding rectangle

            SelectObject(hDC, hBitmapOld);
            DeleteEnhMetaFile(hMeta);    // we are done with this one

            if (info.nEscape) {    // Check if cancelled
                DeleteObject(hBitmap);
                DeleteDC(hDC);
                strcpy_s(info.szLastError,"Cancelled");
                return false;
            }

            // the Bitmap now has the image.
            // Create our DIB and convert the DDB into DIB
            if (!Create(cx, cy, bpp, CXIMAGE_FORMAT_WMF)) {
                DeleteObject(hBitmap);
                DeleteDC(hDC);
                return false;
            }

#if XMF_SUPPORT_TRANSPARENCY
            if (bpp == 24) {
                RGBQUAD    rgbTrans = { XMF_RGBQUAD_TRANSPARENT };
                SetTransColor(rgbTrans);
            }
#endif
            // We're finally ready to get the DIB. Call the driver and let
            // it party on our bitmap. It will fill in the color table,
            // and bitmap bits of our global memory block.
            bRet = GetDIBits(hDC, hBitmap, 0,
                    (uint32_t)cy, GetBits(), (LPBITMAPINFO)pDib, DIB_RGB_COLORS);

            DeleteObject(hBitmap);
            DeleteDC(hDC);

            return (bRet!=0);
        } else {
            DeleteDC(hDC);
        }
    } else {
        if (hBitmap) DeleteObject(hBitmap);
    }

    DeleteEnhMetaFile(hMeta);

    return false;
}

/**********************************************************************
 Function:    CheckMetafileHeader
 Purpose:    Check if the Metafileheader of a file is valid
**********************************************************************/
BOOL CxImageWMF::CheckMetafileHeader(METAFILEHEADER *metafileheader)
{
    uint16_t    *pw;
    uint16_t    cs;
    int32_t        i;

    // check magic #
    if (metafileheader->key != 0x9ac6cdd7L)    return false;

    // test checksum of header
    pw = (uint16_t *)metafileheader;
    cs = *pw;
    pw++;
    for (i = 0; i < 9; i++)    {
        cs ^= *pw;
        pw++;
    }

    if (cs != metafileheader->checksum)    return false;

    // check resolution
    if ((metafileheader->inch <= 0) || (metafileheader->inch > 2540)) return false;

    return true;
}

/**********************************************************************
 Function:    ConvertWmfFiletoEmf
 Purpose:    Converts a Windows Metafile into an Enhanced Metafile
**********************************************************************/
HENHMETAFILE CxImageWMF::ConvertWmfFiletoEmf(CxFile *fp, METAFILEHEADER *metafileheader)
{
    HENHMETAFILE    hMeta;
    uint32_t    lenFile;
    uint32_t    len;
    uint8_t            *p;
    METAHEADER        mfHeader;
    uint32_t            seekpos;

    hMeta = 0;

    // get length of the file
    lenFile = fp->Size();

    // a placeable metafile starts with a METAFILEHEADER
    // read it and check metafileheader
    len = fp->Read(metafileheader, 1, sizeof(METAFILEHEADER));
    if (len < sizeof(METAFILEHEADER)) return (hMeta);

    if (CheckMetafileHeader(metafileheader)) {
        // This is a placeable metafile 
        // Convert the placeable format into something that can
        // be used with GDI metafile functions 
        seekpos = sizeof(METAFILEHEADER);
    } else {
        // Not a placeable wmf. A windows metafile?
        // at least not scaleable.
        // we could try to convert, but would loose ratio. don't allow this
        return (hMeta);

        //metafileheader->bbox.right = ?;
        //metafileheader->bbox.left = ?;
        //metafileheader->bbox.bottom = ?;
        //metafileheader->bbox.top = ?;
        //metafileheader->inch = ?;
        //
        //seekpos = 0;
        // fp->Seek(0, SEEK_SET);    // rewind
    }

    // At this point we have a metaheader regardless of whether
    // the metafile was a windows metafile or a placeable metafile
    // so check to see if it is valid. There is really no good
    // way to do this so just make sure that the mtType is either
    // 1 or 2 (memory or disk file) 
    // in addition we compare the length of the METAHEADER against
    // the length of the file. if filelength < len => no Metafile

    len = fp->Read(&mfHeader, 1, sizeof(METAHEADER));
    if (len < sizeof(METAHEADER)) return (hMeta);

    if ((mfHeader.mtType != 1) && (mfHeader.mtType != 2)) return (hMeta);

    // Length in Bytes from METAHEADER
    len = mfHeader.mtSize * 2;
    if (len > lenFile) return (hMeta);

    // Allocate memory for the metafile bits 
    p = (uint8_t *)malloc(len);
    if (!p)    return (hMeta);

    // seek back to METAHEADER and read all the stuff at once
    fp->Seek(seekpos, SEEK_SET);
    lenFile = fp->Read(p, 1, len);
    if (lenFile != len)    {
        free(p);
        return (hMeta);
    }

    // the following (commented code)  works, but adjusts rclBound of the
    // Enhanced Metafile to full screen.
    // the METAFILEHEADER from above is needed to scale the image

//    hMeta = SetWinMetaFileBits(len, p, nullptr, nullptr);

    // scale the metafile (pixels/inch of metafile => pixels/inch of display)

    METAFILEPICT    mfp;
    int32_t cx1, cy1;
    HDC hDC;

    hDC = ::GetDC(0);
    cx1 = ::GetDeviceCaps(hDC, LOGPIXELSX);
    cy1 = ::GetDeviceCaps(hDC, LOGPIXELSY);

    memset(&mfp, 0, sizeof(mfp));

    mfp.mm = MM_ANISOTROPIC;
    mfp.xExt = 10000; //(metafileheader->bbox.right - metafileheader->bbox.left) * cx1 / metafileheader->inch;
    mfp.yExt = 10000; //(metafileheader->bbox.bottom - metafileheader->bbox.top) * cy1 / metafileheader->inch;
    mfp.hMF = 0;

    // in MM_ANISOTROPIC mode xExt and yExt are in MM_HIENGLISH
    // MM_HIENGLISH means: Each logical unit is converted to 0.001 inch
    //mfp.xExt *= 1000;
    //mfp.yExt *= 1000;
    // ????
    //int32_t k = 332800 / ::GetSystemMetrics(SM_CXSCREEN);
    //mfp.xExt *= k;    mfp.yExt *= k;

    // fix for Win9x
    while ((mfp.xExt < 6554) && (mfp.yExt < 6554))
    {
        mfp.xExt *= 10;
        mfp.yExt *= 10;
    }

    hMeta = SetWinMetaFileBits(len, p, hDC, &mfp);

    if (!hMeta){ //try 2nd conversion using a different mapping
        mfp.mm = MM_TEXT;
        hMeta = SetWinMetaFileBits(len, p, hDC, &mfp);
    }

    ::ReleaseDC(0, hDC);

    // Free Memory
    free(p);

    return (hMeta);
}
/////////////////////////////////////////////////////////////////////
HENHMETAFILE CxImageWMF::ConvertEmfFiletoEmf(CxFile *pFile, ENHMETAHEADER *pemfh)
{
    HENHMETAFILE    hMeta;
    int32_t iLen = pFile->Size();

    // Check the header first: <km>
    int32_t pos = pFile->Tell();
    int32_t iLenRead = pFile->Read(pemfh, 1, sizeof(ENHMETAHEADER));
    if (iLenRead < sizeof(ENHMETAHEADER))         return nullptr;
    if (pemfh->iType != EMR_HEADER)               return nullptr;
    if (pemfh->dSignature != ENHMETA_SIGNATURE)   return nullptr;
    //if (pemfh->nBytes != (uint32_t)iLen)             return nullptr;
    pFile->Seek(pos,SEEK_SET);

    uint8_t* pBuff = (uint8_t *)malloc(iLen);
    if (!pBuff)    return (FALSE);

    // Read the Enhanced Metafile
    iLenRead = pFile->Read(pBuff, 1, iLen);
    if (iLenRead != iLen) {
        free(pBuff);
        return nullptr;
    }

    // Make it a Memory Metafile
    hMeta = SetEnhMetaFileBits(iLen, pBuff);

    free(pBuff);    // finished with this one

    if (!hMeta)    return nullptr;    // oops.

    // Get the Enhanced Metafile Header
    uint32_t uRet = GetEnhMetaFileHeader(hMeta,                // handle of enhanced metafile 
                                sizeof(ENHMETAHEADER),    // size of buffer, in bytes 
                                pemfh);                 // address of buffer to receive data  
  
    if (!uRet) {
        DeleteEnhMetaFile(hMeta);
        return nullptr;
    }

    return (hMeta);
}
////////////////////////////////////////////////////////////////////////////////
#endif //CXIMAGE_SUPPORT_DECODE
////////////////////////////////////////////////////////////////////////////////
#if CXIMAGE_SUPPORT_ENCODE
/////////////////////////////////////////////////////////////////////
bool CxImageWMF::Encode(CxFile * hFile)
{
    if (hFile == nullptr) return false;
    strcpy_s(info.szLastError, "Save WMF not supported");
    return false;
}
#endif    // CXIMAGE_SUPPORT_ENCODE
/////////////////////////////////////////////////////////////////////

/**********************************************************************
Function:    ShrinkMetafile
Purpose:    Shrink the size of a metafile to be not larger than
            the definition
**********************************************************************/
void CxImageWMF::ShrinkMetafile(int32_t &cx, int32_t &cy)
{
    int32_t    xScreen = XMF_MAXSIZE_CX;
    int32_t    yScreen = XMF_MAXSIZE_CY;

    if (cx > xScreen){
        cy = cy * xScreen / cx;
        cx = xScreen;
    }

    if (cy > yScreen){
        cx = cx * yScreen / cy;
        cy = yScreen;
    }
}

#endif    // CIMAGE_SUPPORT_WMF

